Este documento descreve a implementação atualizada do Azure Blob Storage para gerenciamento de anexos na API CordenaAi com contêineres hierárquicos por ambiente. O sistema permite upload, download, listagem e gerenciamento de arquivos com separação automática entre desenvolvimento e produção.
Cliente → AnexosBlobController → AnexoBlobService → BlobStorageService → Azure Storage
O sistema detecta automaticamente o ambiente ASP.NET Core e aplica os prefixos corretos aos contêineres.
{
"ConnectionStrings": {
"AzureStorage": "DefaultEndpointsProtocol=https;AccountName=cordenaaistg13344;AccountKey=SUA_CHAVE_AQUI;EndpointSuffix=core.windows.net"
},
"AzureStorage": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=cordenaaistg13344;AccountKey=SUA_CHAVE_AQUI;EndpointSuffix=core.windows.net",
"Environment": "dev"
}
}
{
"AzureStorage": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=cordenaaistg13344;AccountKey=SUA_CHAVE_PROD;EndpointSuffix=core.windows.net",
"Environment": "prod"
}
}
Todos os contêineres hierárquicos foram criados no Azure Storage Account cordenaaistg13344:
Status: Sistema pronto para uso! Os uploads serão automaticamente direcionados para os contêineres corretos baseado no ambiente.
Para referência futura, os comandos utilizados foram:
# Contêineres de desenvolvimento
az storage container create --name dev-anexos --account-name cordenaaistg13344
az storage container create --name dev-imagens --account-name cordenaaistg13344
az storage container create --name dev-documentos --account-name cordenaaistg13344
az storage container create --name dev-temporarios --account-name cordenaaistg13344
az storage container create --name dev-public-thumbnails --account-name cordenaaistg13344
# Contêineres de produção
az storage container create --name prod-anexos --account-name cordenaaistg13344
az storage container create --name prod-imagens --account-name cordenaaistg13344
az storage container create --name prod-documentos --account-name cordenaaistg13344
az storage container create --name prod-temporarios --account-name cordenaaistg13344
az storage container create --name prod-public-thumbnails --account-name cordenaaistg13344
A configuração agora usa AzureStorageConfig e detecta automaticamente o ambiente:
// Configurar Azure Blob Storage com nova estrutura hierárquica
EstaAplicacao.GravarLog("|debug|Configurando Azure Blob Storage com nova estrutura...", "Startup.ConfigureServices()");
// Configurar AzureStorageConfig via IOptions
services.Configure<AzureStorageConfig>(
Configuration.GetSection("AzureStorage"));
var azureStorageSection = Configuration.GetSection("AzureStorage");
var connectionString = azureStorageSection["ConnectionString"];
var environment = azureStorageSection["Environment"];
EstaAplicacao.GravarLog($"|info|Ambiente detectado: {environment}", "Startup.ConfigureServices()");
if (!string.IsNullOrEmpty(connectionString))
{
// Registrar serviço com nova implementação hierárquica
services.AddScoped<IBlobStorageService, BlobStorageService>();
services.AddScoped<AnexoBlobService>();
EstaAplicacao.GravarLog($"|sucesso|Azure Blob Storage configurado com ambiente: {environment}", "Startup.ConfigureServices()");
}
else
{
EstaAplicacao.GravarLog("|aviso|Azure Storage connection string não encontrada - registrando implementação mock", "Startup.ConfigureServices()");
// Registrar uma implementação mock para evitar erro de DI
services.AddScoped<IBlobStorageService, MockBlobStorageService>();
}
Todos os endpoints existentes /api/anexos-blob/* continuam funcionando exatamente como antes! A migração é transparente:
dev-*prod-*Quando você faz um upload via POST /api/anexos-blob/upload:
_blobStorageService.GetContainerByFileType(contentType)ContainerType (ex: image/jpeg → ContainerType.Imagens)O parâmetro tipoContextoId do upload identifica o tipo de documento. A lista de tipos ativos pode ser obtida via GET /api/TipoContexto/ativos. Referência:
| ID | Nome | Descrição |
|---|---|---|
| 1 | RG | Registro Geral de Identidade |
| 2 | CPF | Cadastro de Pessoa Física |
| 3 | CNH | Carteira Nacional de Habilitação |
| 4 | COMPROVANTE_RESIDENCIA | Comprovante de Residência |
| 5 | CERTIDAO_NASCIMENTO | Certidão de Nascimento |
| 6 | CERTIDAO_CASAMENTO | Certidão de Casamento |
| 7 | COMPROVANTE_ESCOLARIDADE | Comprovante de Escolaridade |
| 8 | ATESTADO_MEDICO | Atestado Médico |
| 9 | DECLARACAO_SAUDE | Declaração de Saúde |
| 10 | AUTORIZACAO_PAIS | Autorização dos Pais/Responsáveis |
| 11 | FOTO_3X4 | Foto 3x4 |
| 12 | CONTRATO | Contrato |
| 13 | TERMO_COMPROMISSO | Termo de Compromisso |
| 14 | FOTO_PERFIL | Foto de Perfil |
| 15 | OUTROS | Outros Documentos |
| 16 | CARTEIRINHA_LOGO | Logo da instituição ou entidade responsável |
POST /api/anexos-blob/upload
Content-Type: multipart/form-data
Authorization: Bearer {token}
Parâmetros:
- file: IFormFile (obrigatório)
- tipoContextoId: int (obrigatório) — ID do tipo de contexto; ver seção "Tipos de Contexto" ou GET /api/TipoContexto/ativos
- entidade: string (obrigatório)
- usuarioUpload: string (opcional)
- tipoAnexo: int (opcional)
Resposta: 200 OK com AnexoBlobResponse
Conflito: 409 se já existe documento para usuário/contexto
GET /api/anexos-blob
Authorization: Bearer {token}
Resposta: Lista de todos os anexos ativos
GET /api/anexos-blob/usuario/{userId}
Authorization: Bearer {token}
Resposta: Lista de anexos do usuário específico
GET /api/anexos-blob/usuario/{userId}/contexto/{contexto}
Authorization: Bearer {token}
Resposta: Anexo específico por usuário e contexto
GET /api/anexos-blob/{id}
Authorization: Bearer {token}
Resposta: Anexo específico ou 404 se não encontrado
PUT /api/anexos-blob/{id}
Authorization: Bearer {token}
Body: AtualizarAnexoBlobRequest
Resposta: Anexo atualizado ou 404 se não encontrado
DELETE /api/anexos-blob/{id}
Authorization: Bearer {token}
Resposta: Confirmação de exclusão (soft delete)
GET /api/anexos-blob/{id}/download
Authorization: Bearer {token}
Resposta: Stream do arquivo para download
GET /api/anexos-blob/{id}/sas-url?expiryHours=1
Authorization: Bearer {token}
Resposta: URL SAS temporária (padrão: 1 hora)
namespace InMinds.CordenaAi.Api.Infra.BlobStorage;
public class AzureStorageConfig
{
public string ConnectionString { get; set; } = string.Empty;
public string Environment { get; set; } = string.Empty;
/// <summary>
/// Gera o nome do contêiner com prefixo do ambiente
/// Exemplo: "dev" + "imagens" = "dev-imagens"
/// </summary>
public string GetContainerName(string baseContainer)
{
return $"{Environment}-{baseContainer}";
}
}
namespace InMinds.CordenaAi.Api.Infra.BlobStorage;
/// <summary>
/// Tipos de contêineres disponíveis no Azure Blob Storage
/// </summary>
public enum ContainerType
{
/// <summary>Imagens (JPG, PNG, GIF, etc.)</summary>
Imagens,
/// <summary>Documentos (PDF, DOC, DOCX, etc.)</summary>
Documentos,
/// <summary>Anexos gerais</summary>
Anexos,
/// <summary>Arquivos temporários</summary>
Temporarios,
/// <summary>Thumbnails públicos</summary>
PublicThumbnails
}
public class UploadAnexoRequest
{
[Required(ErrorMessage = "Arquivo é obrigatório")]
public IFormFile File { get; set; } = null!;
[Required(ErrorMessage = "Contexto é obrigatório")]
[StringLength(255, ErrorMessage = "Contexto deve ter no máximo 255 caracteres")]
public string Contexto { get; set; } = string.Empty;
[Required(ErrorMessage = "Entidade é obrigatória")]
[StringLength(255, ErrorMessage = "Entidade deve ter no máximo 255 caracteres")]
public string Entidade { get; set; } = string.Empty;
[StringLength(255, ErrorMessage = "Usuário upload deve ter no máximo 255 caracteres")]
public string? UsuarioUpload { get; set; }
public int? TipoAnexo { get; set; }
}
public class AnexoBlobResponse
{
public int Id { get; set; }
public string NomeDoArquivo { get; set; } = string.Empty;
public string TipoDoArquivo { get; set; } = string.Empty;
public long Tamanho { get; set; }
public string BlobUrl { get; set; } = string.Empty;
public string BlobContainer { get; set; } = string.Empty;
public string BlobFileName { get; set; } = string.Empty;
public string Contexto { get; set; } = string.Empty;
public string Entidade { get; set; } = string.Empty;
public string UsuarioUpload { get; set; } = string.Empty;
public DateTime DataUpload { get; set; }
public int? TipoAnexo { get; set; }
public bool IsActive { get; set; }
public string? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class AtualizarAnexoBlobRequest
{
[Required(ErrorMessage = "Nome do arquivo é obrigatório")]
[StringLength(255, ErrorMessage = "Nome do arquivo deve ter no máximo 255 caracteres")]
public string NomeDoArquivo { get; set; } = string.Empty;
[StringLength(255, ErrorMessage = "Contexto deve ter no máximo 255 caracteres")]
public string? Contexto { get; set; }
public int? TipoAnexo { get; set; }
}
[Table("anexos")]
public class AnexoEntity
{
[Key]
[Column("id")]
public int Id { get; set; }
[Required]
[Column("NomeDoArquivo")]
[StringLength(255)]
public string NomeDoArquivo { get; set; } = string.Empty;
[Required]
[Column("TipoDoArquivo")]
[StringLength(255)]
public string TipoDoArquivo { get; set; } = string.Empty;
[Column("Tamanho")]
public int? Tamanho { get; set; }
[Required]
[Column("Contexto")]
[StringLength(255)]
public string Contexto { get; set; } = string.Empty;
[Required]
[Column("Entidade")]
[StringLength(255)]
public string Entidade { get; set; } = string.Empty;
[Required]
[Column("UsuarioUpload")]
[StringLength(255)]
public string UsuarioUpload { get; set; } = string.Empty;
[Required]
[Column("DataUpload")]
public DateTime DataUpload { get; set; }
[Column("TipoAnexo")]
public int? TipoAnexo { get; set; }
// Campos específicos para Blob Storage
[Column("BlobUrl")]
[StringLength(500)]
public string? BlobUrl { get; set; }
[Column("BlobContainer")]
[StringLength(100)]
public string? BlobContainer { get; set; }
[Column("BlobFileName")]
[StringLength(255)]
public string? BlobFileName { get; set; }
[Column("IsActive")]
public bool IsActive { get; set; } = true;
[Column("CreatedBy")]
[StringLength(255)]
public string? CreatedBy { get; set; }
[Column("UpdatedAt")]
public DateTime? UpdatedAt { get; set; }
}
A interface IBlobStorageService foi expandida mantendo compatibilidade total com métodos antigos:
public interface IBlobStorageService
{
// ===== NOVOS MÉTODOS v2.0 (ContainerType) =====
Task<string> UploadAsync(Stream fileStream, string fileName, ContainerType containerType);
Task<Stream> DownloadAsync(string fileName, ContainerType containerType);
Task<bool> DeleteAsync(string fileName, ContainerType containerType);
Task<List<string>> ListFilesAsync(ContainerType containerType);
Task<bool> ExistsAsync(string fileName, ContainerType containerType);
// ===== MÉTODOS DE COMPATIBILIDADE (mantidos) =====
Task<string> UploadFileAsync(IFormFile file, string container);
Task<bool> DeleteFileAsync(string blobUrl);
Task<Stream> DownloadFileAsync(string blobUrl);
Task<string> GenerateSasUrlAsync(string blobUrl, TimeSpan expiry);
string GetContainerByFileType(string contentType); // 🔥 AGORA COM PREFIXOS!
}
O método GetContainerByFileType agora retorna nomes com prefixo de ambiente:
// Exemplo em Development (Environment = "dev"):
GetContainerByFileType("image/jpeg") // Retorna: "dev-imagens"
GetContainerByFileType("application/pdf") // Retorna: "dev-documentos"
GetContainerByFileType("text/plain") // Retorna: "dev-anexos"
// Exemplo em Production (Environment = "prod"):
GetContainerByFileType("image/jpeg") // Retorna: "prod-imagens"
GetContainerByFileType("application/pdf") // Retorna: "prod-documentos"
GetContainerByFileType("text/plain") // Retorna: "prod-anexos"
public class AnexoBlobService
{
// Métodos principais implementados:
Task<AnexoBlobResponse> UploadAnexoAsync(UploadAnexoRequest request);
Task<IEnumerable<AnexoBlobResponse>> ListarTodosAsync();
Task<AnexoBlobResponse?> ObterPorIdAsync(int id);
Task<IEnumerable<AnexoBlobResponse>> ListarPorUsuarioAsync(string userId);
Task<AnexoBlobResponse?> ObterPorUsuarioEContextoAsync(string userId, string contexto);
Task<AnexoBlobResponse?> AtualizarAsync(int id, AtualizarAnexoBlobRequest request);
Task<bool> ExcluirAsync(int id);
Task<Stream?> DownloadFileAsync(int id);
Task<string?> GenerateSasUrlAsync(int id, TimeSpan expiry);
Task<bool> ExistePorUsuarioEContextoAsync(string userId, string contexto);
}
imagens/ para image/*documentos/ para PDF, DOC, DOCXanexos/ para outros tipos{
"message": "Descrição do erro",
"error": "Detalhes técnicos"
}
Erro de conexão com Azure
Upload falha
Download não funciona
# Verificar logs da aplicação
dotnet run --verbosity detailed
# Logs do Azure Storage
az storage blob list --container-name anexos --account-name cordenaaistg13344
# Verificar containers criados
az storage container list --account-name cordenaaistg13344
Versão: 1.0
Última Atualização: Outubro de 2025
Responsável: Equipe de Desenvolvimento CordenaAi
Status: ✅ Implementado e Testado
A migração para contêineres hierárquicos foi 100% concluída! O sistema agora:
Status: 🚀 Sistema pronto para produção! Todos os uploads serão automaticamente organizados nos contêineres corretos.